/**
 * Copyright (c) iwindplus Technologies Co., Ltd.2024-2030, All rights reserved.
 */

package com.iwindplus.gateway.server.filter;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.date.StopWatch;
import cn.hutool.core.text.CharSequenceUtil;
import cn.hutool.core.util.ObjectUtil;
import com.iwindplus.boot.domain.constant.CommonConstant.SymbolConstant;
import com.iwindplus.boot.domain.enums.BizCodeEnum;
import com.iwindplus.boot.domain.exception.BizException;
import com.iwindplus.gateway.server.domain.constant.GatewayConstant.FilterConstant;
import com.iwindplus.gateway.server.domain.property.GatewayProperty;
import com.iwindplus.gateway.server.util.GatewayUtil;
import com.iwindplus.mgt.client.system.IpBlackListClient;
import jakarta.annotation.Resource;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.context.annotation.Lazy;
import org.springframework.core.Ordered;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import reactor.core.scheduler.Schedulers;
import reactor.util.retry.Retry;

/**
 * IP黑名单过滤器.
 *
 * @author zengdegui
 * @since 2024-9-24
 */
@Slf4j
@Component
public class IpBlackListFilter implements Ordered, GlobalFilter, ApplicationRunner {

    @Resource
    private GatewayProperty property;

    @Lazy
    @Resource
    private IpBlackListClient ipBlackListClient;

    private volatile Set<String> directIps = Set.of();
    private volatile Set<String> cidrIps = Set.of();

    @Override
    public int getOrder() {
        return FilterConstant.FILTER_IP_BLACK_LIST_ORDER;
    }

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        // 开关未启用跳过当前过滤器
        if (Boolean.FALSE.equals(this.property.getIpBlackList().getEnabled())) {
            return chain.filter(exchange);
        }

        String realIp = GatewayUtil.getRealIp(exchange);
        if (CharSequenceUtil.isBlank(realIp)) {
            return chain.filter(exchange);
        }

        if (log.isInfoEnabled()) {
            // 统计执行时间任务
            StopWatch stopWatch = new StopWatch();
            stopWatch.start(IpBlackListFilter.class.getSimpleName());

            this.validate(realIp);

            return chain.filter(exchange)
                .doOnTerminate(() -> {
                    stopWatch.stop();
                    log.info(stopWatch.prettyPrint(TimeUnit.MILLISECONDS));
                });
        } else {
            this.validate(realIp);

            return chain.filter(exchange);
        }
    }

    private void validate(String realIp) {
        final boolean matched = this.checkIp(realIp);
        if (matched) {
            log.warn("IP 被拦截: {}", realIp);
            throw new BizException(BizCodeEnum.IP_IS_BLACKLISTED, new Object[]{realIp});
        }
    }

    private boolean checkIp(String realIp) {
        if (CollUtil.isEmpty(this.directIps) && CollUtil.isEmpty(this.cidrIps)) {
            this.refreshBlackList();
        }
        return directIps.contains(realIp) || cidrIps.stream().anyMatch(range -> range.contains(realIp));
    }

    /**
     * 刷新黑名单
     */
    public void refreshBlackList() {
        Set<String> directSet = new HashSet<>(16);
        Set<String> cidrSet = new HashSet<>(16);

        Mono.justOrEmpty(this.ipBlackListClient.listIp())
            .retryWhen(Retry.backoff(this.property.getRemoteRetryCount(), this.property.getRemoteRetryPeriod()))
            .subscribe(data -> {
                final List<String> bizData = data.getBizData();
                if (CollUtil.isNotEmpty(bizData)) {
                    for (String ip : bizData) {
                        if (ip.contains(SymbolConstant.SLASH)) {
                            cidrSet.add(ip);
                        } else {
                            directSet.add(ip);
                        }
                    }
                }
            });

        // 更新缓存
        if (ObjectUtil.isNotEmpty(directSet)) {
            this.directIps = directSet;

            log.info("API白名单刷新成功，当前IP条目数: {}", directSet.size());
        }

        if (ObjectUtil.isNotEmpty(cidrSet)) {
            this.cidrIps = cidrSet;

            log.info("IP黑名单刷新成功，当前IP网段条目数: {}", cidrSet.size());
        }
    }

    @Override
    public void run(ApplicationArguments args) throws Exception {
        Schedulers.boundedElastic().schedule(this::refreshBlackList);
    }

}
